package data

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/go-kratos/kratos/v2/log"
	"go-skeleton/app/admin/service/internal/biz"
	"go-skeleton/app/admin/service/internal/data/ent"
	"go-skeleton/app/admin/service/internal/data/ent/adminuser"
	"go-skeleton/pkg/util"
	"go-skeleton/pkg/util/captcha"
	"go-skeleton/pkg/util/pagination"
	"math/rand"
	"strconv"
	"time"
)

type AdminUserRepo struct {
	data *Data
	log  *log.Helper
}

func NewAdminUserRepo(data *Data, logger log.Logger) biz.AdminUserRepo {
	return &AdminUserRepo{
		data: data,
		log:  log.NewHelper(log.With(logger, "module", "data/admin/user")),
	}
}

var adminUserCacheKey = func(key string) string {
	return "au:cache:" + key
}

var sendCodeKey = func(phone string) string {
	return "au:send_code:" + phone
}

func (r *AdminUserRepo) Captcha(ctx context.Context) (string, string, error) {
	store := captcha.NewRedisStore(ctx, r.data.rdb, 10*time.Minute)
	return captcha.MakeStringCaptcha(store)
}

func (r *AdminUserRepo) VerifyCaptcha(ctx context.Context, captchaId, verifyCode string) bool {
	store := captcha.NewRedisStore(ctx, r.data.rdb, 10*time.Minute)
	return captcha.VerifyCaptcha(store, captchaId, verifyCode)
}

func (r *AdminUserRepo) Save(ctx context.Context, g *ent.AdminUser) (*ent.AdminUser, error) {
	ph, err := util.HashPassword(g.Password)
	if err != nil {
		return nil, err
	}

	phoneEncrypt := util.PhoneEncrypt(g.Phone)
	target, err := r.data.db.AdminUser.Create().
		SetNickName(g.NickName).
		SetAvatar(g.Avatar).
		SetPassword(ph).
		SetPhone(phoneEncrypt).
		SetIsNoPower(g.IsNoPower).
		SetAuthRole(g.AuthRole).
		Save(ctx)
	if err != nil {
		return nil, err
	}

	r.setUserCache(ctx, target, adminUserCacheKey(strconv.FormatInt(g.ID, 10)))
	r.setUserCache(ctx, target, adminUserCacheKey(phoneEncrypt))
	return target, nil
}

func (r *AdminUserRepo) Update(ctx context.Context, g *ent.AdminUser) (*ent.AdminUser, error) {
	db := r.data.db.AdminUser.UpdateOneID(g.ID)
	if !util.PhoneIsEncode(g.Phone) {
		db = db.SetPhone(util.PhoneEncrypt(g.Phone))
	}

	if g.Password != "" {
		ph, err := util.HashPassword(g.Password)
		if err != nil {
			return nil, err
		}

		db = db.SetPassword(ph)
	}
	target, err := db.
		SetNickName(g.NickName).
		SetAvatar(g.Avatar).
		SetIsNoPower(g.IsNoPower).
		SetAuthRole(g.AuthRole).
		Save(ctx)
	if err != nil {
		return nil, err
	}

	r.setUserCache(ctx, target, adminUserCacheKey(strconv.FormatInt(g.ID, 10)))
	r.setUserCache(ctx, target, target.Phone)
	return target, nil
}

func (r *AdminUserRepo) FindByID(ctx context.Context, id int64) (*ent.AdminUser, error) {
	cacheKey := adminUserCacheKey(strconv.FormatInt(id, 10))
	target, err := r.getUserFromCache(ctx, cacheKey)

	if err != nil {
		target, err = r.data.db.AdminUser.Get(ctx, id)
		if err != nil {
			return nil, biz.ErrAdminUserNotFound
		}

		// set cache
		r.setUserCache(ctx, target, cacheKey)
	}
	target.Phone = util.PhoneDecrypt(target.Phone)

	return target, nil
}

func (r *AdminUserRepo) FindByPhone(ctx context.Context, phone string) (*ent.AdminUser, error) {
	cacheKey := adminUserCacheKey(util.PhoneEncrypt(phone))
	target, err := r.getUserFromCache(ctx, cacheKey)

	if err != nil {
		target, err = r.data.db.AdminUser.Query().Where(adminuser.PhoneEQ(util.PhoneEncrypt(phone))).Only(ctx)
		if err != nil {
			return nil, biz.ErrAdminUserNotFound
		}

		// set cache
		r.setUserCache(ctx, target, cacheKey)
	}
	target.Phone = util.PhoneDecrypt(target.Phone)

	return target, nil
}

func (r *AdminUserRepo) getUserFromCache(ctx context.Context, key string) (*ent.AdminUser, error) {
	result, err := r.data.rdb.Get(ctx, key).Result()
	if err != nil {
		return nil, err
	}
	var cacheUser = &ent.AdminUser{}
	err = json.Unmarshal([]byte(result), cacheUser)
	if err != nil {
		return nil, err
	}
	return cacheUser, nil
}

func (r *AdminUserRepo) setUserCache(ctx context.Context, adminUser *ent.AdminUser, key string) {
	marshal, err := json.Marshal(adminUser)
	if err != nil {
		r.log.Errorf("fail to set user cache:json.Marshal(%v) error(%v)", adminUser, err)
	}
	err = r.data.rdb.Set(ctx, key, string(marshal), time.Minute*30).Err()
	if err != nil {
		r.log.Errorf("fail to set user cache:redis.Set(%v) error(%v)", adminUser, err)
	}
}

func (r *AdminUserRepo) SendCode(ctx context.Context, phone string) error {
	cacheKey := sendCodeKey(phone)
	code, err := r.data.rdb.Get(ctx, cacheKey).Result()
	if err != nil || code == "" {
		code = fmt.Sprintf("%06v", rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(1000000))
		err = r.data.rdb.Set(ctx, cacheKey, code, time.Minute).Err()
		if err != nil {
			r.log.Errorf("fail to set code cache:redis.Set(%v) error(%v)", code, err)
		}

		return nil
	}
	return biz.ErrAdminVerifyCodeFrequent
}

func (r *AdminUserRepo) VerifyCode(ctx context.Context, phone, code string) (bool, error) {
	cacheKey := sendCodeKey(phone)
	verifyCode, err := r.data.rdb.Get(ctx, cacheKey).Result()
	r.data.rdb.Del(ctx, cacheKey)
	if err != nil || verifyCode == "" || verifyCode != code {
		return false, biz.ErrAdminVerifyCodeErr
	}
	return true, nil
}

func (r *AdminUserRepo) VerifyPassword(ctx context.Context, u *ent.AdminUser) (bool, error) {
	po, err := r.FindByPhone(ctx, u.Phone)
	if err != nil {
		return false, err
	}
	return util.CheckPasswordHash(u.Password, po.Password), nil
}

func (r *AdminUserRepo) List(ctx context.Context, nickName string, pageNum, pageSize int) (userList []*ent.AdminUser, count int, err error) {
	db := r.data.db.AdminUser.Query()
	count, err = db.Count(ctx)
	if err != nil {
		return
	}

	if count == 0 {
		return
	}

	if nickName != "" {
		db.Where(adminuser.NickNameContains(nickName))
	}

	if pageNum == 0 {
		pageNum = 1
	}
	userList, err = db.Order(ent.Asc(adminuser.FieldID)).Offset(pagination.GetPageOffset(pageNum, pageSize)).Limit(pageSize).All(ctx)
	if err != nil {
		return
	}

	return
}

func (r *AdminUserRepo) Del(ctx context.Context, id int64) error {
	return r.data.db.AdminUser.DeleteOneID(id).Exec(ctx)
}
